﻿namespace Microsoft.Web.Helpers
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Web.WebPages.Scope;
    using Microsoft.WindowsAzure;
    using Microsoft.WindowsAzure.StorageClient;

    public static class WindowsAzureStorage
    {
        private const string LocalBlobServiceUrl = @"http://127.0.0.1:10000";
        private const string LocalTableServiceUrl = @"http://127.0.0.1:10002";        
        private const string DevStorageAccount = "devstoreaccount1";
        private const string AzureTableServiceDomain = "table.core.windows.net";
        private const string AzureBlobServiceDomain = "blob.core.windows.net";

        private static readonly object _accountName;
        private static readonly object _accountKey;
        
        static WindowsAzureStorage() 
        {
            _accountName = new object();
            _accountKey = new object();
        }

        /// <summary>
        /// The name of the Windows Azure Storage account
        /// </summary>
        public static string AccountName
        {
            get
            {                
                return (string)(ScopeStorage.CurrentScope[_accountName] ?? String.Empty);
            }

            set
            {
                ScopeStorage.CurrentScope[_accountName] = value;
            }
        }

        /// <summary>
        /// The shared key of the Windows Azure Storage account
        /// </summary>
        public static string AccountKey
        {
            get
            {
                return (string)(ScopeStorage.CurrentScope[_accountKey] ?? String.Empty);
            }

            set
            {
                ScopeStorage.CurrentScope[_accountKey] = value;
            }
        }

        /// <summary>
        /// Retrieves a single row from a table in the storage account
        /// </summary>
        /// <param name="tableName">The name of the table to be queried</param>
        /// <param name="partitionKey">The partition-key value of the row</param>
        /// <param name="rowKey">The row-key value of the row</param>
        public static dynamic GetRow(string tableName, string partitionKey, string rowKey)
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();
            string rowPath = tableName + "(PartitionKey='" + partitionKey + "',RowKey='" + rowKey + "')";

            IList<dynamic> result = OData.Get(serviceUrl + "/" + rowPath, new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey));

            return result.SingleOrDefault();
        }

        /// <summary>
        /// Retrieves the rows of a table in the storage account
        /// </summary>
        /// <param name="tableName">The name of the table to be queried</param>
        /// <param name="whereOptions">(optional) The filter options to be applied over the table</param>
        /// <param name="take">(optional) The maximum number of rows to be retrieved (0 = "all rows")</param>
        public static IList<dynamic> GetRows(string tableName, string whereOptions = "", int take = 0)
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();

            OData.ODataQuery query = OData.Open(serviceUrl + "/" + tableName, new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey));

            if (!string.IsNullOrEmpty(whereOptions))
            {                
                query = query.Where(whereOptions);
            }

            if (take > 0)
            {
                query = query.Top(take);
            }

            return query.Get();
        }

        /// <summary>
        /// Checks the existence of a table in the storage account
        /// </summary>
        /// <param name="tableName">The name of the table to check</param>
        public static bool TableExists(string tableName)
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();

            IList<dynamic> tables = OData.Get(serviceUrl + "/Tables", new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey));

            return (tables.FirstOrDefault(t => t.TableName == tableName) != null);
        }

        /// <summary>
        /// Retrieves the Tables in the storage account
        /// </summary>      
        public static IList<dynamic> GetTables()
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();

            return OData.Get(serviceUrl + "/Tables", new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey));
        }

        /// <summary>
        /// Creates a new instance that represents the row of a table
        /// </summary>
        /// <param name="partitionKey">The partition-key value of the row</param>
        /// <param name="rowKey">The row-key value of the row</param>
        public static dynamic CreateRow(string partitionKey, string rowKey)
        {
            dynamic row = OData.CreateEntity();
            row.PartitionKey = partitionKey;
            row.RowKey = rowKey;

            return row;
        }

        /// <summary>
        /// Creates a new table in the storage account
        /// </summary>
        /// <param name="tableName">The name of the table to be created</param>
        public static void CreateTable(string tableName)
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();

            dynamic table = OData.CreateEntity();
            table.TableName = tableName;

            OData.Insert(serviceUrl + "/Tables", table, new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey));
        }

        /// <summary>
        /// Deletes an existing table from the storage account
        /// </summary>
        /// <param name="tableName">The name of the table to be deleted</param>
        public static void DeleteTable(string tableName)
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();

            OData.Delete(serviceUrl + "/Tables('" + tableName + "')", new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey));
        }

        /// <summary>
        /// Inserts a new row into a table
        /// </summary>        
        /// <param name="tableName">The name of the table where the new row will be inserted</param>
        /// <param name="row">The instance that represents the new row to be inserted</param>
        public static void InsertRow(string tableName, dynamic row)
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();

            OData.Insert(serviceUrl + "/" + tableName, row, new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey));
        }

        /// <summary>
        /// Updates an existing row in a table
        /// </summary>
        /// <param name="tableName">The name of the table within the row</param>
        /// <param name="row">The instance that represents the row to be updated</param>
        public static void UpdateRow(string tableName, dynamic row)
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();
            
            string rowPath = tableName + "(PartitionKey='" + row.PartitionKey + "',RowKey='" + row.RowKey + "')";

            OData.Update(serviceUrl + "/" + rowPath, row, new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey), "*");
        }

        /// <summary>
        /// Deletes an existing row in a table
        /// </summary>
        /// <param name="tableName">The name of the table within the row</param>
        /// <param name="partitionKey">The partition-key value of the row</param>
        /// <param name="rowKey">The row-key value of the row</param>
        public static void DeleteRow(string tableName, string partitionKey, string rowKey)
        {
            ValidateAccount();
            string serviceUrl = GetTableServiceUrl();

            string rowPath = tableName + "(PartitionKey='" + partitionKey + "',RowKey='" + rowKey + "')";

            OData.Delete(serviceUrl + "/" + rowPath, new SharedKeyLiteAuthenticationScheme(AccountName, AccountKey), "*");
        }

        /// <summary>
        /// Uploads binary data to a Blob
        /// </summary>
        /// <param name="blobAddress">The address of the blob</param>
        /// <param name="stream">The data stream to be uploaded</param>
        public static void UploadBinaryToBlob(string blobAddress, Stream stream)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);
            blob.UploadFromStream(stream);
        }

        /// <summary>
        /// Uploads binary data to a Blob
        /// </summary>
        /// <param name="blobAddress">The address of the blob</param>
        /// <param name="content">The array of bytes to be uploaded</param>
        public static void UploadBinaryToBlob(string blobAddress, byte[] content)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);
            blob.UploadByteArray(content);
        }

        /// <summary>
        /// Uploads binary data to a Blob
        /// </summary>
        /// <param name="blobAddress">The address of the blob</param>
        /// <param name="fileName">The path of the file to be uploaded</param>
        public static void UploadBinaryToBlob(string blobAddress, string fileName)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);
            blob.UploadFile(fileName);
        }

        /// <summary>
        /// Uploads a string of text to a Blob
        /// </summary>
        /// <param name="blobAddress">The address of the blob</param>
        /// <param name="content">The string of text to be uploaded</param>
        public static void UploadTextToBlob(string blobAddress, string content)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);
            blob.UploadText(content);
        }

        /// <summary>
        /// Downloads the blob contents as a string of text
        /// </summary>
        /// <param name="blobAddress">The address of the blob</param>
        public static string DownloadBlobAsText(string blobAddress)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);

            return blob.DownloadText();
        }

        /// <summary>
        /// Downloads the blob contents to a stream
        /// </summary>
        /// <param name="blobAddress">The address of the blob</param>
        /// <param name="stream">The target stream</param>
        public static void DownloadBlobAsStream(string blobAddress, Stream stream)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);

            blob.DownloadToStream(stream);
        }

        /// <summary>
        /// Downloads the blob contents to a file
        /// </summary>
        /// <param name="blobAddress">The address of the blob</param>
        /// <param name="fileName">The name of the file</param>
        public static void DownloadBlobToFile(string blobAddress, string fileName)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);

            blob.DownloadToFile(fileName);
        }

        /// <summary>
        /// Downloads the blob contents as an array of bytes
        /// </summary>
        /// <param name="blobAddress">The address of the blob</param>
        public static byte[] DownloadBlobAsByteArray(string blobAddress)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);

            return blob.DownloadByteArray();
        }

        /// <summary>
        /// Creates a new blob container in the storage account
        /// </summary>
        /// <param name="containerAddress">The address of the blob container</param>
        public static void CreateBlobContainer(string containerAddress)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlobContainer container = blobClient.GetContainerReference(containerAddress);

            container.CreateIfNotExist();
        }

        /// <summary>
        /// Retrieves a list of all the blobs in a blob container
        /// </summary>
        /// <param name="containerAddress">The address of the blob container</param>
        public static IList<string> ListBlobs(string containerAddress)
        {
            ValidateAccount();
            Uri serviceUrl = new Uri(GetBlobServiceUrl());

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlobContainer container = blobClient.GetContainerReference(containerAddress);

            return container.ListBlobs(new BlobRequestOptions() { UseFlatBlobListing = true })
                            .Select(b => serviceUrl.MakeRelativeUri(b.Uri).OriginalString)
                            .ToList();
        }

        /// <summary>
        /// Retrieves a list of all the blob containers in the storage account
        /// </summary>
        public static IList<string> ListBlobContainers()
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));

            return blobClient.ListContainers().Select(c => c.Name).ToList();
        }

        /// <summary>
        /// Deletes an existing blob container from the storage account
        /// </summary>
        /// <param name="containerAddress">The address of the blob container</param>
        public static void DeleteBlobContainer(string containerAddress)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlobContainer container = blobClient.GetContainerReference(containerAddress);

            container.Delete();
        }

        /// <summary>
        /// Deletes an existing blob
        /// </summary>
        /// <param name="containerAddress">The address of the blob</param>
        public static void DeleteBlob(string blobAddress)
        {
            ValidateAccount();
            string serviceUrl = GetBlobServiceUrl();

            CloudBlobClient blobClient = new CloudBlobClient(serviceUrl, new StorageCredentialsAccountAndKey(AccountName, AccountKey));
            CloudBlob blob = blobClient.GetBlobReference(blobAddress);

            blob.DeleteIfExists();
        }

        private static string GetTableServiceUrl()
        {
            return (AccountName == DevStorageAccount) ? LocalTableServiceUrl + "/" + AccountName + "/"
                                                      : @"https://" + AccountName + "." + AzureTableServiceDomain + "/";
        }

        private static string GetBlobServiceUrl()
        {
            return (AccountName == DevStorageAccount) ? LocalBlobServiceUrl + "/" + AccountName + "/"
                                                      : @"https://" + AccountName + "." + AzureBlobServiceDomain + "/";
        }

        private static void ValidateAccount()
        {
            if (string.IsNullOrEmpty(AccountName))
            {
                throw new ArgumentNullException("accountName", "The AccountName property has not been specified.");
            }

            if (string.IsNullOrEmpty(AccountKey))
            {
                throw new ArgumentNullException("accountKey", "The AccountKey property has not been specified.");
            }
        }
    }
}
